import multiprocessing
from random import random


def simple_factory(cls, args):
    if isinstance(args, dict):
        return cls(**args)
    elif isinstance(args, (tuple, list)):
        return cls(*args)
    elif isinstance(args, (int, float)):
        return cls(args)
    else:
        raise TypeError(f'Argument for {cls} must be a dict, a number, a tuple'
                        f', or a list, but got a {type(args)} ({args}).')


class EpochBasedCoefficient(object):
    def __init__(self, start_epoch, warmup_epoch, **kwargs):
        self.start_epoch = start_epoch
        self.warmup_epoch = max(1, warmup_epoch)

    def get_coef(self, epoch: int):
        if self.start_epoch < 0 or epoch < self.start_epoch:
            return 0
        return min(1, (epoch - self.start_epoch + 1) / self.warmup_epoch)

    def __repr__(self):
        return (f'{self.__class__.__name__}(start_epoch={self.start_epoch}'
                f', warmup_epoch={self.warmup_epoch})')


class EpochBasedNumber(object):
    def __init__(self, value, start_epoch=0, warmup_epoch=-1, **kwargs):
        self.final_val = value
        self.coef = EpochBasedCoefficient(start_epoch, warmup_epoch)
        self.val = multiprocessing.Value('f', 0)
        self.set_epoch(0)

    def set_epoch(self, epoch: int):
        self.val.value = self.coef.get_coef(epoch) * self.final_val

    def value(self):
        return self.val.value

    def __float__(self):
        return self.val.value

    def __str__(self):
        return f'{self.val.value:.5f}'.rstrip('0').rstrip('.')

    def __repr__(self):
        return (f'{self.__class__.__name__}(final_val={self.final_val}'
                f', coef={self.coef}, val={round(self.val.value, 5)})')


class EpochBasedActivator(object):
    def __init__(self, prob=1, start_epoch=0, warmup_epoch=-1, **kwargs):
        self.prob = EpochBasedNumber(prob, start_epoch, warmup_epoch)

    def set_epoch(self, epoch: int):
        self.prob.set_epoch(epoch)

    def enabled(self):
        return float(self.prob) > 0

    def activated(self):
        return random() < float(self.prob)

    def possible(self):
        return self.prob.final_val > 0

    def __str__(self):
        return f'{self.prob.value():.3f}'.rstrip('0').rstrip('.')

    def __repr__(self):
        return f'{self.__class__.__name__}(prob={repr(self.prob)})'


class MpInteger(object):
    def __init__(self, num=0):
        self.val = multiprocessing.Value('i', int(num))
        self.lock = multiprocessing.Lock()  # for augmented addition

    def __int__(self):
        return int(self.val.value)

    def __iadd__(self, other: int):
        with self.lock:
            self.val.value += int(other)
        return self

    def __eq__(self, other: int):
        return self.val.value == int(other)

    def __call__(self):
        return self.val.value

    def __str__(self):
        return f'{self.val.value}'

    def __bool__(self):
        return self.val.value != 0

    def reset(self, num=0):
        self.val.value = int(num)


class PercentageStat(object):
    def __init__(self, num=0, den=0):
        self.num = MpInteger(num)
        self.den = MpInteger(den)

    def __str__(self):
        if self.den == 0:
            return '0%'
        percent = f'{self.num() * 100 / self.den():.2f}'.rstrip('0').rstrip('.')
        return f'{percent}%'
        return '0%' if self.den == 0 else f'{self.num() / self.den():.2%}'

    def __repr__(self):
        return f'{self.__class__.__name__}(num={self.num()}, den={self.den()})'

    def reset(self, num=0, den=0):
        self.num.reset(num)
        self.den.reset(den)
